package service

import (
	"context"
	"database/sql"

	dbmodel "gitee.com/hp-huiw/family-expense-api/internal/db/model"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
)

type BankCardService interface {
	Count(ctx context.Context, accountGroupID string) (int64, error)
	QueryAllWithBasicInfo(ctx context.Context, accountGroupID string) ([]dto.BankCardResponse, error)
	QueryBankCardInfo(ctx context.Context, accountGroupID string, id string) (*dto.BankCardResponse, error)
	QueryBasicInfo(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64) ([]dto.BankCardResponse, error)
	Add(ctx context.Context, accountGroupID string, data *dto.AddBankCardRequest) error
	Update(ctx context.Context, accountGroupID string, data *dto.UpdateBankCardRequest) error
	Delete(ctx context.Context, accountGroupID string, id string) (bool, error)
	BatchDelete(ctx context.Context, accountGroupID string, ids []string) error
	DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error
}

// NewBankCardService creates a new instance of BankCardService.
func NewBankCardService(db *sql.DB) BankCardService {
	return &bankCardService{
		db:                         db,
		bankCardRepository:         repository.NewBankCardRepository(db),
		userService:                NewUserService(db),
		bankService:                NewBankService(db),
		incomeExpenseDetailService: NewIncomeExpenseDetailService(db),
	}
}

// Count counts the bank card by accountGroupID.
func (s *bankCardService) Count(ctx context.Context, accountGroupID string) (int64, error) {
	return s.bankCardRepository.Count(ctx, accountGroupID)
}

// QueryAllWithBasicInfo queries all bank cards with basic info which associated with provided account group.
func (s *bankCardService) QueryAllWithBasicInfo(ctx context.Context, accountGroupID string) ([]dto.BankCardResponse, error) {
	query, err := s.bankCardRepository.QueryAllWithBasicInfo(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.BankCard, _ int) dto.BankCardResponse {
		return dto.BankCardResponse{
			ID:            item.ID,
			Name:          item.Name,
			IsCreditCard:  item.IsCreditCard,
			Enabled:       item.Enabled,
			StatementDate: item.StatementDate,
			BankName:      item.BankName,
			BankLogoUrl:   s.bankService.GetLogoUrl(item.BankShortName),
		}
	})

	return result, nil
}

// QueryBankCardInfo queries bank card based on provided id.
func (s *bankCardService) QueryBankCardInfo(ctx context.Context, accountGroupID string, id string) (*dto.BankCardResponse, error) {
	query, err := s.bankCardRepository.QueryBankCardInfo(ctx, accountGroupID, id)

	if err != nil {
		return nil, err
	}

	return &dto.BankCardResponse{
		ID:                    query.ID,
		Name:                  query.Name,
		IsCreditCard:          query.IsCreditCard,
		Enabled:               query.Enabled,
		StatementDate:         query.StatementDate,
		PaymentDueDate:        query.PaymentDueDate,
		CustomerServiceNumber: query.CustomerServiceNumber,
		BankID:                query.BankID,
		BankName:              query.BankName,
		BankLogoUrl:           s.bankService.GetLogoUrl(query.BankShortName),
	}, nil
}

// QueryBasicInfo queries all bank cards basic info which associated with provided account group.
func (s *bankCardService) QueryBasicInfo(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64) ([]dto.BankCardResponse, error) {
	query, err := s.bankCardRepository.QueryBasicInfo(ctx, accountGroupID, &pageSize, &pageIndex)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.BankCard, _ int) dto.BankCardResponse {
		return dto.BankCardResponse{
			ID:            item.ID,
			Name:          item.Name,
			IsCreditCard:  item.IsCreditCard,
			Enabled:       item.Enabled,
			StatementDate: item.StatementDate,
			BankLogoUrl:   s.bankService.GetLogoUrl(item.BankShortName),
		}
	})

	return result, nil
}

// Add adds new bank card for account group.
func (s *bankCardService) Add(ctx context.Context, accountGroupID string, data *dto.AddBankCardRequest) error {
	bankCard := createBankCardModel(&data.BankCardFields)
	bankCard.AccountGroupID = accountGroupID
	bankCard.DateCreated = date.GetCurrentDate()

	return s.bankCardRepository.Insert(ctx, bankCard)
}

// Update updates bank card info based on provided map data and its id.
func (s *bankCardService) Update(ctx context.Context, accountGroupID string, data *dto.UpdateBankCardRequest) error {
	bankCard := createBankCardModel(&data.BankCardFields)
	return s.bankCardRepository.Update(ctx, accountGroupID, data.ID, bankCard)
}

// Delete deletes the BankCard with provided id.
func (s *bankCardService) Delete(ctx context.Context, accountGroupID string, id string) (bool, error) {
	exists, err := s.incomeExpenseDetailService.Exists(ctx, accountGroupID, id)

	if err != nil {
		return false, err
	}

	// Checks if this bank card is used by income/expense.
	if !exists {
		// If this bank card is not used then deletes it.
		if err := s.bankCardRepository.Delete(ctx, accountGroupID, id); err != nil {
			return false, err
		}
	}

	return true, nil
}

// BatchDelete deletes the BankCards with provides list of id.
func (s *bankCardService) BatchDelete(ctx context.Context, accountGroupID string, ids []string) error {
	// Queries the input id list and makes sure they belong to the specific account group.
	// This is an example shows how to secure the data by taking extra action.
	// This can void other account group users call api directly and provide these ids which they don't actually own them.
	ids, err := s.bankCardRepository.QueryIDs(ctx, ids, accountGroupID)

	if err != nil {
		return err
	}

	// Queries all bank card ids which used by income/expense.
	usedIDs, err := s.incomeExpenseDetailService.QueryUsedBankCardIDs(ctx, ids, accountGroupID)

	if err != nil {
		return err
	}

	// Excludes all used bank card ids.
	unUsedIDs := util.SliceExcludes(ids, usedIDs)

	if err := s.bankCardRepository.BatchDelete(ctx, accountGroupID, unUsedIDs); err != nil {
		return err
	}

	return nil
}

// DeleteWithTx deletes all bank cards by account group id with transaction.
func (s *bankCardService) DeleteWithTx(ctx context.Context, tx *sql.Tx, accountGroupID string) error {
	return s.bankCardRepository.DeleteWithTx(ctx, tx, accountGroupID)
}

// createBankCardModel creates model.BankCard from dto.BankCardRequest.
func createBankCardModel(data *dto.BankCardFields) *dbmodel.BankCard {
	return &dbmodel.BankCard{
		ID:                    util.GenerateUUIDWithoutDashes(),
		BankID:                data.Bank.ID,
		Name:                  data.Name,
		StatementDate:         &data.StatementDate,
		PaymentDueDate:        &data.PaymentDueDate,
		CustomerServiceNumber: data.CustomerServiceNumber,
		IsCreditCard:          *data.IsCreditCard,
		Enabled:               *data.Enabled,
		LastUpdated:           date.GetCurrentDate(),
	}
}

// Container of BankCardService functions.
type bankCardService struct {
	db                         *sql.DB
	bankCardRepository         repository.BankCardRepository
	userService                UserService
	bankService                BankService
	incomeExpenseDetailService IncomeExpenseDetailService
}
